package com.google.code.xconf.schema.definition;

import com.google.code.xconf.Property;
import com.google.code.xconf.PropertyGroup;
import com.google.code.xconf.schema.SchemaDefinition;
import com.google.code.xconf.schema.SchemaLayoutException;
import com.google.code.xconf.schema.SchemaSettings;
import com.google.code.xconf.schema.SchemaValidationException;
import com.google.code.xconf.utils.NumberTools;
import com.google.code.xconf.utils.StringTools;
import com.google.code.xconf.schema.definition.value.options.ExtendedBooleanModes;
import static com.google.code.xconf.schema.SchemaConstants.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class PropertyDefinition extends SchemaDefinition {

    private boolean isGroup;
    private String identifier;
    private Set<String> aliases = new HashSet<>();

    private Integer maxPropertyValues;
    private boolean strictPropertyValues;

    private SchemaDefinition nodesDefinition;

    public PropertyDefinition(PropertyGroup group, SchemaSettings schemaSettings, boolean isGroup) {
        super(isGroup? PROPERTY_GROUP : PROPERTY, group, schemaSettings);
        this.isGroup = isGroup;
        configure();
    }

    @Override
    protected List<String> getValidNodeNames() {
        return Arrays.asList(IDENTIFIER, ALIAS, ALIASES, SAME_NAMED_PROPERTIES_ALLOWED, OPTIONAL,
                IDENTIFIER_CASE_SENSITIVE, PROPERTY_VALUE_CASE_SENSITIVE, MAX_PROPERTY_VALUES,
                STRICT_PROPERTY_VALUES, PROPERTY_VALUES, PROPERTIES_DEFINITION);
    }

    public void configure() {
        settings = settings.snapshot();
        settings.setSameNamedPropertiesAllowed(resolveSameNamedPropertiesAllowed());
        settings.setPropertyOptional(resolveOptional());
        settings.setPropertyIdentifierCaseSensitive(resolveIdentifierCaseSensitive());
        settings.setPropertyValueCaseSensitive(resolvePropertyValueCaseSensitive());
        identifier = resolveIdentifier();
        aliases.addAll(resolveAliases());
        strictPropertyValues = resolveStrictPropertyValues();
        maxPropertyValues = resolveMaxPropertyValues();
        nodesDefinition = resolveNodesDefinition();
        if (nodesDefinition == null) {
            if (!isGroup && strictPropertyValues) {
                throw new SchemaLayoutException("The " + PROPERTY_VALUES + " definition is missing for " + definition.getName() + " " + identifier + ". Set '" + STRICT_PROPERTY_VALUES + " false' if you do not wish to declare property values");
            } else if (isGroup) {
                throw new SchemaLayoutException("The " + PROPERTIES_DEFINITION + " definition is missing for " + definition.getName() + " " + identifier);
            }
        }
        if (!isGroup) {
            if (maxPropertyValues == null) {
                if (strictPropertyValues) {
                    maxPropertyValues = ((PropertyValuesDefinition) nodesDefinition).getMaxValueDefinitions();
                } else {
                    maxPropertyValues = Integer.MAX_VALUE;
                }
            }
        }
    }

    public void validate(Property property) throws SchemaValidationException {
        if (!matchIdentifier(property.getName())) {
            throw new SchemaValidationException("Invalid identifier '" + property.getName() + "', expected '" + resolveIdentifier() + "'");
        }
        if (isGroup) {
            if (!(property instanceof PropertyGroup)) {
                throw new SchemaValidationException("Invalid property type '" + property.getName() + "', expected 'PropertyGroup'");
            }
            ((PropertiesDefinition) nodesDefinition).validate(((PropertyGroup) property).getNodes());
        } else {
            if (property instanceof PropertyGroup) {
                throw new SchemaValidationException("Invalid property type '" + property.getName() + "', expected 'Property'");
            }
            if (property.getValues().size() > maxPropertyValues) {
                throw new SchemaValidationException("Number of maximum values exceeded for property '" + property.getName() + "'");
            }
            if (strictPropertyValues) {
                ((PropertyValuesDefinition) nodesDefinition).validate(property);
            }
        }
    }

    public boolean isGroup() {
        return isGroup;
    }

    public Collection<String> getIdentifiers() {
        Set<String> identifiers = new HashSet<>();
        identifiers.add(identifier);
        identifiers.addAll(aliases);
        return identifiers;
    }

    public boolean matchIdentifier(String id) {
        for (String thisid : getIdentifiers()) {
            if ((isPropertyIdentifierCaseSensitive() && thisid.equals(id)) || thisid.equalsIgnoreCase(id)) {
                return true;
            }
        }
        return false;
    }

    private String resolveIdentifier() {
        String tagValue = null;
        if (definition.hasValues()) {
            tagValue = definition.getValue(0);
        }
        String propertyValue = resolveDefinitionPropertyString(IDENTIFIER, tagValue != null);
        if (tagValue == null && propertyValue == null) {
            throw new SchemaLayoutException("Missing identifier property");
        }
        return propertyValue != null ? propertyValue : tagValue;
    }

    private Set<String> resolveAliases() {
        Set<String> aliases = new HashSet<>();
        for (Property property : definition.getProperties(ALIAS)) {
            if (!property.hasValues()) {
                throw new SchemaLayoutException("Property '" + ALIAS + "' cannot be empty");
            }
            if (property.numValues() > 1) {
                throw new SchemaLayoutException("Property '" + ALIAS + "' cannot contain more than one value");
            }
            aliases.add(property.firstValue());
        }
        for (Property property : definition.getProperties(ALIASES)) {
            if (!property.hasValues()) {
                throw new SchemaLayoutException("Property '" + ALIASES + "' cannot be empty");
            }
            aliases.addAll(property.getValues());
        }
        return aliases;
    }

    private boolean resolveSameNamedPropertiesAllowed() {
        return resolveDefinitionPropertyBoolean(SAME_NAMED_PROPERTIES_ALLOWED, true, isSameNamedPropertiesAllowed());
    }

    private boolean resolveOptional() {
        return resolveDefinitionPropertyBoolean(OPTIONAL, true, isPropertyOptional());
    }

    private boolean resolveIdentifierCaseSensitive() {
        return resolveDefinitionPropertyBoolean(IDENTIFIER_CASE_SENSITIVE, true, isPropertyIdentifierCaseSensitive());
    }

    private boolean resolvePropertyValueCaseSensitive() {
        return resolveDefinitionPropertyBoolean(PROPERTY_VALUE_CASE_SENSITIVE, true, isPropertyValueCaseSensitive());
    }

    private boolean resolveStrictPropertyValues() {
        if (definition.containsPropertyGroup(PROPERTY_VALUES)) {
            return true;
        }
        String value = resolveDefinitionPropertyString(STRICT_PROPERTY_VALUES, true);
        if (value != null) {
            if (!new ExtendedBooleanModes().matches(value, true)) {
                throw new SchemaLayoutException("Property '" + STRICT_PROPERTY_VALUES + "' value not of boolean type");
            }
            return ExtendedBooleanModes.isTrue(value, true);
        }
        return true;
    }

    private Integer resolveMaxPropertyValues() {
        String value = resolveDefinitionPropertyString(MAX_PROPERTY_VALUES, true);
        if (value != null) {
            if (!StringTools.isNumberAssignable(value)) {
                throw new SchemaLayoutException("Property '" + MAX_PROPERTY_VALUES + "' value not of type Integer");
            }
            return NumberTools.intValue(value);
        } else {
            return null;
        }
    }

    private SchemaDefinition resolveNodesDefinition() {
        String target = isGroup ? PROPERTIES_DEFINITION : PROPERTY_VALUES;
        Collection<PropertyGroup> groups = definition.getPropertyGroups(target);
        if (!groups.isEmpty()) {
            if (groups.size() > 1) {
                throw new SchemaLayoutException("Only one " + target + " specification can be declared");
            }
            if (isGroup) {
                return new PropertiesDefinition(definition.getPropertyGroups(PROPERTIES_DEFINITION).iterator().next(), settings);
            } else {
                return new PropertyValuesDefinition(definition.getPropertyGroups(PROPERTY_VALUES).iterator().next(), settings);
            }
        }
        return null;
    }

}
